/*
 * Copyright (C) 2011 Jason von Nieda <jason@vonnieda.org>
 * 
 * This file is part of OpenPnP.
 * 
 * OpenPnP is free software: you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * OpenPnP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with OpenPnP. If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * For more information about OpenPnP visit http://openpnp.org
 */

package org.openpnp.model;

import java.util.Locale;

import org.simpleframework.xml.Attribute;



public class Length {
    public enum Field {
        X, Y, Z
    }

    @Attribute
    private double value;
    @Attribute
    private LengthUnit units;

    public Length() {

    }

    public Length(double value, LengthUnit units) {
        this.value = value;
        this.units = units;
    }

    public Length add(Length length) {
        length = length.convertToUnits(units);
        return new Length(value + length.getValue(), units);
    }

    public Length subtract(Length length) {
        length = length.convertToUnits(units);
        return new Length(value - length.getValue(), units);
    }

    public Length multiply(Length length) {
        length = length.convertToUnits(units);
        return new Length(value * length.getValue(), units);
    }

    public Length add(double d) {
        return new Length(value + d, units);
    }

    public Length subtract(double d) {
        return new Length(value - d, units);
    }

    public Length multiply(double d) {
        return new Length(value * d, units);
    }

    public double getValue() {
        return value;
    }

    public void setValue(double value) {
        this.value = value;
    }

    public LengthUnit getUnits() {
        return units;
    }

    public void setUnits(LengthUnit units) {
        this.units = units;
    }

    public Length convertToUnits(LengthUnit units) {
        if (this.units == units) {
            return this;
        }
        // First convert the current value to millimeters, which we use as a base unit.
        double mm = 0;
        if (this.units == LengthUnit.Millimeters) {
            mm = value;
        }
        else if (this.units == LengthUnit.Centimeters) {
            mm = value * 10;
        }
        else if (this.units == LengthUnit.Meters) {
            mm = value * 1000;
        }
        else if (this.units == LengthUnit.Inches) {
            mm = value * 25.4;
        }
        else if (this.units == LengthUnit.Feet) {
            mm = value * 25.4 * 12;
        }
        else if (this.units == LengthUnit.Mils) {
            mm = value / 1000 * 25.4;
        }
        else if (this.units == LengthUnit.Microns) {
            mm = value / 1000.0;
        }
        else {
            throw new Error("convertLength() unrecognized units " + this.units);
        }

        // Then convert the calculated millimeter value to the requested unit.
        if (units == LengthUnit.Millimeters) {
            return new Length(mm, units);
        }
        else if (units == LengthUnit.Centimeters) {
            return new Length(mm / 10, units);
        }
        else if (units == LengthUnit.Meters) {
            return new Length(mm / 1000, units);
        }
        else if (units == LengthUnit.Inches) {
            return new Length(mm * (1 / 25.4), units);
        }
        else if (units == LengthUnit.Feet) {
            return new Length(mm * (1 / 25.4) * 12, units);
        }
        else if (units == LengthUnit.Mils) {
            return new Length(mm * (1 / 25.4 * 1000), units);
        }
        else if (units == LengthUnit.Microns) {
            return new Length(mm * 1000, units);
        }
        else {
            throw new Error("convertLength() unrecognized units " + units);
        }

    }

    public static double convertToUnits(double value, LengthUnit fromUnits, LengthUnit toUnits) {
        return new Length(value, fromUnits).convertToUnits(toUnits).getValue();
    }

    public static Length parse(String s) {
        return parse(s, false);
    }

    /**
     * Takes a value in the format of a double followed by any number of spaces followed by the
     * shortName of a LengthUnit value and returns the value as a Length object. Returns null if the
     * value could not be parsed.
     */
    public static Length parse(String s, boolean requireUnits) {
        if (s == null) {
            return null;
        }

        s = s.trim();

        Length length = new Length(0, null);
        // find the index of the first character that is not a -, . or digit.
        int startOfUnits = -1;
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch != '-' && ch != '.' && !Character.isDigit(ch)) {
                startOfUnits = i;
                break;
            }
        }

        String valueString = null;
        if (startOfUnits != -1) {
            valueString = s.substring(0, startOfUnits);
            String unitsString = s.substring(startOfUnits);
            unitsString = unitsString.trim();
            for (LengthUnit lengthUnit : LengthUnit.values()) {
                if (lengthUnit.getShortName().equalsIgnoreCase(unitsString)) {
                    length.setUnits(lengthUnit);
                    break;
                }
            }
        }
        else {
            valueString = s;
        }

        if (requireUnits && length.getUnits() == null) {
            return null;
        }

        try {
            double value = Double.parseDouble(valueString);
            length.setValue(value);
        }
        catch (Exception e) {
            return null;
        }

        return length;
    }

    @Override
    public String toString() {
        return String.format(Locale.US, "%2.3f%s", value, units.getShortName());
    }

    /**
     * Performs the same function as toString() but allows the caller to specify the format String
     * that is used. The format String should contain %f and %s in that order for value and
     * units.getShortName().
     * 
     * @param fmt
     * @return
     */
    public String toString(String fmt) {
        if (fmt == null) {
            return toString();
        }
        return String.format(Locale.US, fmt, value, units.getShortName());
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((units == null) ? 0 : units.hashCode());
        long temp;
        temp = Double.doubleToLongBits(value);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Length other = (Length) obj;
        if (units != other.units) {
            return false;
        }
        if (Double.doubleToLongBits(value) != Double.doubleToLongBits(other.value)) {
            return false;
        }
        return true;
    }

    public static Location setLocationField(Configuration configuration, Location location,
            Length length, Field field, boolean defaultToOldUnits) {
        Length oldLength = null;
        if (field == Field.X) {
            oldLength = location.getLengthX();
        }
        else if (field == Field.Y) {
            oldLength = location.getLengthY();
        }
        else if (field == Field.Z) {
            oldLength = location.getLengthZ();
        }
        if (length.getUnits() == null) {
            if (defaultToOldUnits) {
                length.setUnits(oldLength.getUnits());
            }
            if (length.getUnits() == null) {
                length.setUnits(configuration.getSystemUnits());
            }
        }
        if (location.getUnits() == null) {
            throw new Error("This can't happen!");
        }
        else {
            location = location.convertToUnits(length.getUnits());
        }
        if (field == Field.X) {
            location = location.derive(length.getValue(), null, null, null);
        }
        else if (field == Field.Y) {
            location = location.derive(null, length.getValue(), null, null);
        }
        else if (field == Field.Z) {
            location = location.derive(null, null, length.getValue(), null);
        }
        return location;
    }

    /**
     * Sets the specified field on the passed Location object. Enforces application specific unit
     * conversion. If the new Length value does not have units set, this method will set the units
     * of the Length to the system default units. If the Location itself does not have units set,
     * the Location's units are set to the Length's units. Finally, if the Location's units have
     * changed, the entire Location is converted to the new units and the new object is returned.
     * 
     * @param configuration
     * @param location
     * @param length
     * @param field
     * @return
     */
    public static Location setLocationField(Configuration configuration, Location location,
            Length length, Field field) {
        return setLocationField(configuration, location, length, field, false);
    }

}
